/*
 * InvertedListFile.cpp
 *
 *  Created on: Nov 30, 2012
 *      Author: m
 */

#include "InvertedListFile.h"

namespace physical
{

InvertedListFile::InvertedListFile( const char* filename, bool trunc ) :
        m_file( new FixedSizeBlockFile( s_blockSize ) ),
        m_recordSerializer( new InvertedListRecordSerializer ),
        m_recordListSerializer( new InvertedListRecordListSerializer ),
        m_lastBlockWritten( 0 )
{
    if ( trunc == true || !fileExists( filename ) )
    {
        m_file->open( filename,
                ios_base::in | ios_base::out | ios_base::trunc );
        this->initializeControlData();
        this->saveControlData();
    }
    else
    {
        m_file->open( filename, ios_base::in | ios_base::out );
        this->loadControlData();
    }
}

InvertedListFile::~InvertedListFile()
{
    m_file->close();
}

size_t InvertedListFile::addList( term_id_t termID,
        InvertedList& invertedList )
{
    size_t recordSize;

    auto newRecord = InvertedListRecord( termID, invertedList );

    recordSize = m_recordSerializer->getSize( newRecord );

    /*
     * Acá se pide un bloque que tenga capacidad para insertar el nodo
     * y además le quede espacio libre para hacer updates ( 20% extra )
     */
    auto blockID = this->getBlockWithCapacity( recordSize + 0.2 * s_blockSize );

    this->insertRecordInBlock( blockID, newRecord );

    return blockID;
}

InvertedList InvertedListFile::getList( term_id_t termID,
        size_t blockID )
{
    auto record = this->getRecordFromBlock( blockID, termID );

    return record.getInvertedList();
}

size_t InvertedListFile::updateList( term_id_t termID, size_t blockID,
        InvertedList& invertedList )
{
    size_t recordSize;
    size_t newblockID = blockID;

    auto newRecord = InvertedListRecord( termID, invertedList );

    // Intento actualizar el registro
    bool updateSucced = this->updateRecordInBlock( blockID, newRecord );

    // Si falló debo re-ubicarlo (hago lo mismo que en el addInvertedList)
    if ( !updateSucced )
    {
        recordSize = m_recordSerializer->getSize( newRecord );

        newblockID = this->getBlockWithCapacity(
                recordSize + 0.2 * s_blockSize );

        this->insertRecordInBlock( newblockID, newRecord );
    }

    return newblockID;
}

void InvertedListFile::removeList( term_id_t termID, size_t blockID )
{
    this->removeRecordFromBlock( blockID, termID );
}

size_t InvertedListFile::getBlockWithCapacity( size_t recordSize )
{
    size_t blockID = 0;

    // El primer byte de control se reserva para uso futuro
    for ( size_t i = 1; i < s_blockSize; ++i )
    {
        if ( m_freeSpace[i] >= recordSize )
        {
            blockID = i;
            break;
        }
    }

    return blockID;
}

void InvertedListFile::insertRecordInBlock( size_t blockID,
        InvertedListRecord& record )
{
    InvertedListRecordList recordList;
    size_t freeSpaceNeeded;
    size_t totalSpaceUsed;
    byte buffer[s_blockSize];

    // Verifico espacio libre!
    freeSpaceNeeded = m_recordSerializer->getSize( record );
    if ( freeSpaceNeeded > m_freeSpace[blockID] )
    {
        throw NotEnoughFreeSpaceInBlockException();
    }

    if( m_freeSpace[blockID] < s_blockSize - 1 )
    {

		// Leo el bloque del archivo
		m_file->readBlock( blockID, buffer );

		// deserializo la lista de registros internos
		m_recordListSerializer->deserialize( buffer, recordList );

    }

    // inserto el nuevo registro
    recordList.push_back( record );

    // serializo la lista de registros internos
    totalSpaceUsed = m_recordListSerializer->serialize( recordList, buffer );

    // grabo la lista
    m_file->writeBlock( blockID, buffer );

    // actualizo información de control
    m_freeSpace[blockID] = s_blockSize - totalSpaceUsed;

    this->saveControlData();
}

InvertedListRecord InvertedListFile::getRecordFromBlock( size_t blockID,
        term_id_t termID )
{
    InvertedListRecordList recordList;
    byte buffer[s_blockSize];

    // Leo el bloque del archivo
    m_file->readBlock( blockID, buffer );

    // deserializo la lista de registros internos
    m_recordListSerializer->deserialize( buffer, recordList );

    // busco el registro a devolver
    for ( auto it = recordList.begin(); it != recordList.end(); it++ )
    {
        if ( it->getID() == termID )
        {
            return *it;
        }
    }

    /*
     * Si llegué hasta acá es porque el registro no se encontró,
     *  asique tiro una excepción
     */
    throw InvertedListNotFoundInBlockException();
}

bool InvertedListFile::updateRecordInBlock( size_t blockID,
        InvertedListRecord& newRecord )
{
    bool updateSucced;
    InvertedListRecordList recordList;
    byte buffer[s_blockSize];
    size_t oldRecordSize;
    size_t newRecordSize;
    size_t finalBlockSize;

    // Leo el bloque del archivo
    m_file->readBlock( blockID, buffer );

    // deserializo la lista de registros internos
    m_recordListSerializer->deserialize( buffer, recordList );

    // busco registro viejo
    InvertedListRecord* oldRecord = NULL;
    auto it = recordList.begin();

    for ( ; it != recordList.end(); it++ )
    {
        if ( it->getID() == newRecord.getID() )
        {
            oldRecord = &(*it);
            break;
        }
    }

    /*
     * Si oldRecord apunta al final de la lista significa
     *  que la lista invertida a actualizar no está en
     *  ese bloque, por lo tanto tiro una excepción
     */

    if ( oldRecord == NULL )
    {
        throw InvertedListNotFoundInBlockException();
    }

    /*
     * Comparo tamaño del registro viejo y el nuevo
     *  y valido el espacio libre en el bloque, si
     *  el espacio libre alcanza, reemplazo el
     *  registro. Si no, lo borro para que pueda
     *  ser re-insertado en otro (la lógica del if
     *  está invertida!)
     */

    oldRecordSize = m_recordSerializer->getSize( *oldRecord );
    newRecordSize = m_recordSerializer->getSize( newRecord );

    size_t freeSpaceInBlock = m_freeSpace[blockID] + oldRecordSize;

    recordList.erase( it );
    if ( newRecordSize > freeSpaceInBlock )
    {
        updateSucced = false;
    }
    else
    {
        recordList.push_back( newRecord );
        updateSucced = true;
    }

    // Re-escribo el bloque con la información actualizada
    finalBlockSize = m_recordListSerializer->serialize( recordList, buffer );

    m_file->writeBlock( blockID, buffer );

    // Actualizo información de control
    m_freeSpace[blockID] = s_blockSize - finalBlockSize;

    this->saveControlData();

    return updateSucced;
}

void InvertedListFile::removeRecordFromBlock( size_t blockID,
        term_id_t termID )
{
    InvertedListRecordList recordList;
    byte buffer[s_blockSize];
    size_t finalBlockSize;

    // Leo el bloque del archivo
    m_file->readBlock( blockID, buffer );

    // deserializo la lista de registros internos
    m_recordListSerializer->deserialize( buffer, recordList );

    // busco registro viejo
    auto it = recordList.begin();
    for ( ; it != recordList.end(); it++ )
    {
        if ( it->getID() == termID )
        {
            break;
        }
    }
    /*
     * Si it apunta al final de la lista significa
     *  que la lista invertida a actualizar no está en
     *  ese bloque, por lo tanto tiro una excepción
     */
    if ( it != recordList.end() )
    {
        // Elimino el registro
        recordList.erase( it );
    }
    else
    {
        throw InvertedListNotFoundInBlockException();
    }

    // Re-escribo el bloque sin el registro eliminado
    finalBlockSize = m_recordListSerializer->serialize( recordList, buffer );
    m_file->writeBlock( blockID, buffer );

    // Actualizo información de control
    m_freeSpace[blockID] = s_blockSize - finalBlockSize;
    this->saveControlData();
}

void InvertedListFile::saveControlData()
{
    m_file->writeBlock( 0, ( char* ) ( m_freeSpace ) );
}

void InvertedListFile::loadControlData()
{
    m_file->readBlock( 0, ( char* ) ( m_freeSpace ) );
}

void InvertedListFile::initializeControlData()
{
    /*
     * Se le resta 1 byte por el header de la lista
     *  de registros internos
     */
    memset( m_freeSpace, s_blockSize - 1, s_blockSize );
}

} /* namespace physical */
